Padroneggia l'iterazione asincrona in JavaScript usando il ciclo 'for await...of' e helper per iteratori asincroni personalizzati. Migliora l'elaborazione di stream e la gestione dei dati con esempi pratici.
Helper per Iteratori Asincroni JavaScript: For Each - Iterazione per l'Elaborazione di Stream
La programmazione asincrona è una pietra miliare dello sviluppo JavaScript moderno, che consente alle applicazioni di gestire operazioni che richiedono tempo senza bloccare il thread principale. Gli iteratori asincroni, introdotti in ECMAScript 2018, forniscono un potente meccanismo per l'elaborazione asincrona di flussi di dati. Questo post del blog approfondisce il concetto di iteratori asincroni e dimostra come implementare una funzione di supporto 'for each' asincrona per semplificare l'elaborazione degli stream.
Comprendere gli Iteratori Asincroni
Un iteratore asincrono è un oggetto conforme all'interfaccia AsyncIterator. Definisce un metodo next() che restituisce una promise, la quale si risolve in un oggetto con due proprietà:
value: Il valore successivo nella sequenza.done: Un booleano che indica se l'iteratore ha terminato.
Gli iteratori asincroni sono comunemente usati per consumare dati da fonti asincrone come stream di rete, file system o database. Il ciclo for await...of fornisce una sintassi comoda per iterare su iterabili asincroni.
Esempio: Lettura Asincrona da un File
Consideriamo uno scenario in cui è necessario leggere un file di grandi dimensioni riga per riga senza bloccare il thread principale. È possibile raggiungere questo obiettivo utilizzando un iteratore asincrono:
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function processFile(filePath) {
for await (const line of readFileLines(filePath)) {
console.log(`Line: ${line}`);
}
}
// Esempio di utilizzo
processFile('path/to/your/file.txt');
In questo esempio, readFileLines è una funzione generatrice asincrona che restituisce (yield) ogni riga del file man mano che viene letta. La funzione processFile itera quindi sulle righe utilizzando for await...of, elaborando ogni riga in modo asincrono.
Implementare un Helper 'For Each' Asincrono
Sebbene il ciclo for await...of sia utile, può diventare verboso quando è necessario eseguire operazioni complesse su ciascun elemento nello stream. una funzione di supporto 'for each' asincrona può semplificare questo processo incapsulando la logica di iterazione.
Implementazione di Base
Ecco un'implementazione di base di una funzione 'for each' asincrona:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
await callback(item);
}
}
Questa funzione accetta un iterabile asincrono e una funzione di callback come argomenti. Itera sull'iterabile usando for await...of e chiama la funzione di callback per ogni elemento. Anche la funzione di callback dovrebbe essere asincrona se si desidera attendere il suo completamento prima di passare all'elemento successivo.
Esempio: Elaborazione di Dati da un'API
Supponiamo di recuperare dati da un'API che restituisce uno stream di elementi. È possibile utilizzare l'helper 'for each' asincrono per elaborare ogni elemento man mano che arriva:
async function* fetchDataStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
// Supponendo che l'API restituisca chunk JSON
const chunk = decoder.decode(value);
const items = JSON.parse(`[${chunk.replace(/}\{/g, '},{')}]`); //Suddivide i chunk in un array JSON valido
for(const item of items){
yield item;
}
}
} finally {
reader.releaseLock();
}
}
async function processItem(item) {
// Simula un'operazione asincrona
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Sostituire con l'endpoint della tua API
await asyncForEach(fetchDataStream(apiUrl), processItem);
console.log('Finished processing data.');
}
// Esempio di utilizzo
main();
In questo esempio, fetchDataStream recupera i dati dall'API e restituisce (yield) ogni elemento man mano che viene ricevuto. La funzione processItem simula un'operazione asincrona su ciascun elemento. L'helper asyncForEach semplifica quindi la logica di iterazione ed elaborazione.
Miglioramenti e Considerazioni
Gestione degli Errori
È fondamentale gestire gli errori che possono verificarsi durante l'iterazione asincrona. È possibile racchiudere la funzione di callback in un blocco try...catch per intercettare e gestire le eccezioni:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
try {
await callback(item);
} catch (error) {
console.error(`Error processing item: ${item}`, error);
// È possibile scegliere di rilanciare l'errore o continuare l'elaborazione
}
}
}
Controllo della Concorrenza
Per impostazione predefinita, l'helper 'for each' asincrono elabora gli elementi in sequenza. Se è necessario elaborare elementi in modo concorrente, è possibile utilizzare un pool di Promise per limitare il numero di operazioni concorrenti:
async function asyncForEachConcurrent(iterable, callback, concurrency) {
const executing = [];
for await (const item of iterable) {
const p = callback(item).then(() => executing.splice(executing.indexOf(p), 1));
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
}
async function processItem(item) {
// Simula un'operazione asincrona
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Sostituire con l'endpoint della tua API
await asyncForEachConcurrent(fetchDataStream(apiUrl), processItem, 5); // Concorrenza di 5
console.log('Finished processing data.');
}
In questo esempio, asyncForEachConcurrent limita il numero di esecuzioni di callback concorrenti al livello di concorrenza specificato. Ciò può migliorare le prestazioni quando si ha a che fare con grandi flussi di dati.
Annullamento (Cancellation)
In alcuni casi, potrebbe essere necessario annullare prematuramente il processo di iterazione. È possibile raggiungere questo obiettivo utilizzando un AbortController:
async function asyncForEach(iterable, callback, signal) {
for await (const item of iterable) {
if (signal && signal.aborted) {
console.log('Iteration aborted.');
return;
}
await callback(item);
}
}
async function main() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort(); // Annulla dopo 2 secondi
}, 2000);
const apiUrl = 'https://api.example.com/data'; // Sostituire con l'endpoint della tua API
await asyncForEach(fetchDataStream(apiUrl), processItem, signal);
console.log('Finished processing data.');
}
In questo esempio, la funzione asyncForEach controlla la proprietà signal.aborted prima di ogni iterazione. Se il segnale viene annullato, l'iterazione si interrompe.
Applicazioni nel Mondo Reale
Gli iteratori asincroni e l'helper 'for each' asincrono possono essere applicati a una vasta gamma di scenari del mondo reale:
- Pipeline di elaborazione dati: Elaborazione di grandi set di dati da database o file system.
- Stream di dati in tempo reale: Gestione di dati da web socket, code di messaggi o reti di sensori.
- Consumo di API: Recupero ed elaborazione di dati da API che restituiscono stream di elementi.
- Elaborazione di immagini e video: Elaborazione di file multimediali di grandi dimensioni in blocchi (chunk).
- Analisi dei log: Analisi di file di log di grandi dimensioni riga per riga.
Esempio - Dati Azionari Internazionali: Consideriamo un'applicazione che recupera quotazioni azionarie in tempo reale da varie borse internazionali. Un iteratore asincrono può essere utilizzato per lo streaming dei dati, e un 'for each' asincrono può elaborare ogni quotazione, aggiornando l'interfaccia utente con i prezzi più recenti. Questo può essere utilizzato per visualizzare i tassi azionari attuali di aziende come:
- Tencent (Cina): Recupero dei dati azionari di un'importante azienda tecnologica internazionale
- Tata Consultancy Services (India): Visualizzazione degli aggiornamenti azionari di una società leader nei servizi IT
- Samsung Electronics (Corea del Sud): Mostra i tassi azionari di un produttore globale di elettronica
- Toyota Motor Corporation (Giappone): Monitoraggio dei prezzi delle azioni di un produttore automobilistico internazionale
Conclusione
Gli iteratori asincroni e l'helper 'for each' asincrono forniscono un modo potente ed elegante per elaborare flussi di dati in modo asincrono in JavaScript. Incapsulando la logica di iterazione, è possibile semplificare il codice, migliorare la leggibilità e aumentare le prestazioni delle applicazioni. Gestendo gli errori, controllando la concorrenza e abilitando l'annullamento, è possibile creare pipeline di elaborazione dati asincrone robuste e scalabili.